﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows;
using System.Drawing;
using Ignitron.EEngine.Maths;
using Ignitron.EEngine.Lighting;
using Ignitron.EEngine.Model;
using Ignitron.EEngine.Adapters.OpenGL;
using System.Windows.Forms.Integration;


using OpenTK;
using OpenTK.Graphics.OpenGL;


namespace Ignitron.EEngine.Adapters
{
    /// <summary>
    /// Adapter class for OpenTK library
    /// </summary>
    internal class OpenTKAdapter : IEngineExtended
    {
        /// <summary>
        /// The rendering control and its host
        /// </summary>
        private GLControl mGLControl;
        private WindowsFormsHost mHost;

        /// <summary>
        /// Light count attributes
        /// </summary>
        private const int mMaxLights = 8;       
        private int mLightCount = 0;

        /// <summary>
        /// The OpenGL Camera
        /// </summary>
        private Camera mCamera;

        /// <summary>
        /// The selected texture
        /// </summary>
        private OpenTKTexture mCurrentTexture;

        /// <summary>
        /// Back buffer clearing color
        /// </summary>
        private Vector4 mClearColor;

        /// <summary>
        /// OpenTK background
        /// </summary>
        private OpenTKBackground mBackground = null;

        /// <summary>
        /// OpenTK Render-to texture
        /// </summary>
        private OpenTKRenderTexture mRenderTexture = null;

        /// <summary>
        /// The level of alpha opacity
        /// </summary>
        private float mAlphaOpacity;

        /// <summary>
        /// Flag if lighting is enabled
        /// </summary>
        private bool mLightingEnabled;

        /// <summary>
        /// True, when initialized
        /// </summary>
        private bool mInitialized = false;

        /// <summary>
        /// Constructor
        /// </summary>
        public OpenTKAdapter()
        {
            mInitialized = false;
            // create the controls
            mGLControl = new GLControl();
            mHost = new WindowsFormsHost();
            mHost.Child = mGLControl;

            mCamera = new Camera();

            mGLControl.Load += mGLControl_Load;
            mGLControl.Resize += mGLControl_Resize;           

            // bind events
            mGLControl.Paint += mGLControl_Paint;
            mGLControl.MouseDown += mGLControl_MouseDown;
            mGLControl.MouseMove += mGLControl_MouseMove;
            mGLControl.MouseUp += mGLControl_MouseUp;
            mGLControl.MouseWheel += mGLControl_MouseWheel;
        }

        void mGLControl_Load(object sender, EventArgs e)
        {
            // Peform the init of OpenGL subsystem
            GL.EnableClientState(ArrayCap.VertexArray);
            GL.EnableClientState(ArrayCap.NormalArray);
            GL.EnableClientState(ArrayCap.TextureCoordArray);
            GL.Enable(EnableCap.DepthTest);
            GL.PixelStore(PixelStoreParameter.UnpackAlignment, 1);
            GL.Enable(EnableCap.Texture2D);
            GL.ShadeModel(ShadingModel.Smooth);

            GL.Hint(HintTarget.PerspectiveCorrectionHint, HintMode.Nicest);

            mAlphaOpacity = 1.0f;

            mInitialized = true;
        }


        #region IEngineEntended interface implementation

        /// <summary>
        /// True flag, when the initialization process of the engine is completed
        /// </summary>
        public bool IsInitializationCompleted
        {
            get { return mInitialized; }
        }

        /// <summary>
        /// Closes the engine
        /// </summary>
        public void Close()
        {
            // unbind events
            mGLControl.Load -= mGLControl_Load;
            mGLControl.Resize -= mGLControl_Resize;
            mGLControl.Paint -= mGLControl_Paint;

            mGLControl.MouseDown -= mGLControl_MouseDown;
            mGLControl.MouseMove -= mGLControl_MouseMove;
            mGLControl.MouseUp -= mGLControl_MouseUp;
            mGLControl.MouseWheel -= mGLControl_MouseWheel;

            // free the host window
            mHost.Dispose();
            mHost = null;
            mGLControl.Dispose();
            mGLControl = null;

            // free the camera
            if (mCamera != null)
            {
                mCamera = null;
            }

            // free the background
            if (mBackground != null)
            {
                mBackground = null;
            }

            // free render-to-texture object
            if (mRenderTexture != null)
            {
                mRenderTexture.Shutdown();
                mRenderTexture = null;
            }

            // Models, meshes and textures hold no references to OpenGL resources, no need to free them
        }      

        /// <summary>
        /// Returns the WPF control
        /// </summary>
        public DependencyObject Control
        {
            get { return mHost; }
        }

        /// <summary>
        /// Rendering request event
        /// </summary>
        public event EventHandler RenderRequestEvent;

        /// <summary>
        /// Mouse handling events
        /// </summary>
        public event EventHandler<EMouseEventArgs> LeftButtonDownEvent;
        public event EventHandler<EMouseEventArgs> LeftButtonUpEvent;
        public event EventHandler<EMouseEventArgs> RightButtonDownEvent;
        public event EventHandler<EMouseEventArgs> RightButtonUpEvent;
        public event EventHandler<EMouseEventArgs> MouseMoveEvent;
        public event EventHandler<EMouseEventArgs> MouseWheelEvent;

        public IRenderingControl RenderingControl
        {
            get { throw new NotImplementedException(); }
        }

        /// <summary>
        /// Returns the size of the rendering control
        /// </summary>
        public System.Windows.Size GetViewportSize()
        {
            return new System.Windows.Size(mGLControl.Width, mGLControl.Height);
        }

        /// <summary>
        /// Returns the world matrix
        /// </summary>
        public EMatrix4 GetWorldMatrix()
        {
            return Matrix4.Identity.ToEE();
        }

        /// <summary>
        /// Returns the view matrix
        /// </summary>
        public EMatrix4 GetViewMatrix()
        {
            return mCamera.ViewMatrix.ToEE();
        }

        /// <summary>
        /// Returns the projection matrix
        /// </summary>
        public EMatrix4 GetProjectionMatrix()
        {
            Matrix4 perspective = Matrix4.CreatePerspectiveFieldOfView(mCamera.GetFOV(), (float)mGLControl.Width / (float)mGLControl.Height, mCamera.GetNear(), mCamera.GetFar());
            return perspective.ToEE();
        }

        /// <summary>
        /// Captures the mouse
        /// </summary>        
        public void SetCapture()
        {
            mGLControl.Capture = true;
        }

        /// <summary>
        /// Releases the mouse capture
        /// </summary>
        public void ReleaseCapture()
        {
            mGLControl.Capture = false;
        }

        /// <summary>
        /// Prepares the rendering
        /// </summary>
        public void BeginRender()
        {
            // clear the buffers
            GL.ClearColor(mClearColor.X, mClearColor.Y, mClearColor.Z, mClearColor.W);
            GL.Clear(ClearBufferMask.ColorBufferBit | ClearBufferMask.DepthBufferBit);

            // set the perspective matrix
            GL.MatrixMode(MatrixMode.Projection);
            GL.LoadIdentity();
            Matrix4 perspective = Matrix4.CreatePerspectiveFieldOfView(mCamera.GetFOV(), (float)mGLControl.Width / (float)mGLControl.Height, mCamera.GetNear(), mCamera.GetFar());
            GL.MultMatrix(ref perspective);

            // load identity
            GL.MatrixMode(MatrixMode.Modelview);
            GL.PushMatrix();
            GL.LoadIdentity();

            // multiply by the View matrix (camera)
            Matrix4 viewMatrix = mCamera.ViewMatrix;
            GL.MultMatrix(ref viewMatrix);

            // render the background
            if (mBackground != null)
            {
                mBackground.Render(mGLControl.Width, mGLControl.Height, 0, -0.9999f, mLightingEnabled);
            }
        }

        /// <summary>
        /// Refreshes the control
        /// </summary>
        public void EndRender()
        {
            GL.PopMatrix();
            mGLControl.SwapBuffers();
        }

        /// <summary>
        /// Begins rendering points
        /// </summary>
        public void BeginPoints()
        {
            GL.Begin(PrimitiveType.Points);
        }

        /// <summary>
        /// Ends rendering points
        /// </summary>
        public void EndPoints()
        {
            GL.End();
        }

        /// <summary>
        /// Begins rendering lines
        /// </summary>
        public void BeginLines()
        {
            GL.Begin(PrimitiveType.Lines);
        }

        /// <summary>
        /// Ends rendering lines
        /// </summary>
        public void EndLines()
        {
            GL.End();
        }

        /// <summary>
        /// Sets the camera
        /// </summary>
        public void SetCamera(EVector3 position, EVector3 lookAt, EVector3 up)
        {
            mCamera.SetLookAt(position.ToOpenTK(), lookAt.ToOpenTK(), up.ToOpenTK());
        }

        /// <summary>
        /// Sets the FOV of camera
        /// </summary>
        public void SetFOV(float fov)
        {
            mCamera.SetFOV(fov);
        }
        /// <summary>
        /// Sets the near and far limits of the camera
        /// </summary>
        public void SetNearFarLimits(float near, float far)
        {
            mCamera.SetNearFarLimits(near, far);
        }

        /// <summary>
        /// Renders a point using OpenGL
        /// </summary>
        public void RenderPoint(EVector3 p0, EVector4 color)
        {
            GL.Color4(color.ToOpenTK());
            GL.Vertex3(p0.ToOpenTK());
        }

        /// <summary>
        /// Renders a point using OpenGL
        /// </summary>
        public void RenderLine(EVector3 p1, EVector3 p2, EVector4 color)
        {
            GL.Color4(color.ToOpenTK());
            GL.Vertex3(p1.ToOpenTK());
            GL.Vertex3(p2.ToOpenTK());
        }

        /// <summary>
        /// Renders the OpenTK model
        /// </summary>
        public void RenderModel(EModel model)
        {
            if (mCurrentTexture == null)
                throw new ApplicationException("No texture selected.");

            // bind texture 
            mCurrentTexture.BindTexture(mLightingEnabled);

            // render model
            (model as OpenTKModel).SetAlphaOpacity(mAlphaOpacity);
            (model as OpenTKModel).Render();

            // free resources
            mCurrentTexture.FreeAfterBinding();
        }
        
        /// <summary>
        /// Rendering method for normal vectors for the given model
        /// </summary>
        public void RenderNormals(EModel model, float magnitude, EVector4 color)
        {
            (model as OpenTKModel).RenderNormals(color.ToOpenTK(), magnitude);
        }

        /// <summary>
        /// Pushes the current matrix
        /// </summary>
        public void PushMatrix()
        {
            GL.MatrixMode(MatrixMode.Modelview);
            GL.PushMatrix();
        }

        /// <summary>
        /// Multiplies the current matrix
        /// </summary>
        public void MultMatrix(ref EMatrix4 matrix)
        {
            GL.MatrixMode(MatrixMode.Modelview);
            Matrix4 mat = matrix.ToOpenTK();
            GL.MultMatrix(ref mat);
        }

        /// <summary>
        /// Makes the current matrix identity
        /// </summary>
        public void LoadIdentity()
        {
            GL.MatrixMode(MatrixMode.Modelview);
            GL.LoadIdentity();
        }

        /// <summary>
        /// Pops a matrix from the stack
        /// </summary>
        public void PopMatrix()
        {
            GL.MatrixMode(MatrixMode.Modelview);
            GL.PopMatrix();
        }

        /// <summary>
        /// Sets the alpha blend value
        /// </summary>
        public void SetAlphaBlend(float alpha)
        {
            mAlphaOpacity = alpha;
        }

        /// <summary>
        /// Sets the current rendering material parameters for the mesh
        /// </summary>
        public void SetMaterial(EMaterial material)
        {
            GL.Material(MaterialFace.Front, MaterialParameter.Ambient, material.Ambient.ToOpenTK());
            GL.Material(MaterialFace.Front, MaterialParameter.Diffuse, material.Diffuse.ToOpenTK());
            GL.Material(MaterialFace.Front, MaterialParameter.Specular, material.Specular.ToOpenTK());
            GL.Material(MaterialFace.Front, MaterialParameter.Shininess, material.Shininess);
        }

        /// <summary>
        /// Sets the current texture
        /// </summary>
        public void SetTexture(ETexture texture)
        {
            mCurrentTexture = texture as OpenTKTexture;
        }

        /// <summary>
        /// Sets the back buffer clearing color
        /// </summary>
        public void SetClearColor(EVector4 color)
        {
            mClearColor = color.ToOpenTK();
        }

        /// <summary>
        /// Sets the rendered view's background image
        /// </summary>
        public void SetBackground(IBackground background)
        {
            mBackground = new OpenTKBackground(background);
        }

        /// <summary>
        /// Creates a new OpenTK Model
        /// </summary>
        public EModel CreateModel()
        {
            return new OpenTKModel();
        }

        /// <summary>
        /// Creates an OpenTK texture
        /// </summary>
        public ETexture CreateTexture()
        {
            return new OpenTKTexture();
        }

        /// <summary>
        /// Enables or disables lighting
        /// </summary>
        public void EnableLighting(bool enabled)
        {
            if (enabled)
            {
                GL.Enable(EnableCap.Lighting);
            }
            else
            {
                GL.Disable(EnableCap.Lighting);
            }

            mLightingEnabled = enabled;
        }

        /// <summary>
        /// Creates a new light
        /// </summary>
        public ELight CreateLight()
        {
            if (mLightCount >= mMaxLights)
                throw new Exception("Light count overflow.");
            return new ELight(mLightCount++);
        }

        /// <summary>
        /// Renders a light
        /// </summary>
        public void RenderLight(ELight light)
        {
            if (light.IsTurnedOn == false)
            {
                GL.Disable(light.LightIndex.ToLightEnableCap());
            }
            else
            {
                GL.Enable(light.LightIndex.ToLightEnableCap());
                LightName lightName = light.LightIndex.ToLightName();

                GL.Light(lightName, LightParameter.Position, light.Position.ToOpenTK());
                GL.Light(lightName, LightParameter.Ambient, light.Ambient.ToOpenTK());
                GL.Light(lightName, LightParameter.Diffuse, light.Diffuse.ToOpenTK());
                GL.Light(lightName, LightParameter.Specular, light.Specular.ToOpenTK());
                GL.Light(lightName, LightParameter.ConstantAttenuation, light.ConstantAttenuation);
                GL.Light(lightName, LightParameter.LinearAttenuation, light.LinearAttenuation);
                GL.Light(lightName, LightParameter.QuadraticAttenuation, light.QuadraticAttenuation);
            }
        }

        /// <summary>
        /// Sets the global ambient light
        /// </summary>
        public void SetGlobalAmbient(EVector3 color)
        {
            GL.LightModel(LightModelParameter.LightModelAmbient, new float[] { color.X, color.Y, color.Z, 1f } );
        }

        /// <summary>
        /// Initializes the rendering texture
        /// </summary>
        public void InitializeRenderingTexture(int width, int height)
        {
            // clean-up first
            if (mRenderTexture != null)
            {
                mRenderTexture.Shutdown();
                mRenderTexture = null;
            }

            // init a new render texture
            mRenderTexture = new OpenTKRenderTexture(width, height);
        }

        /// <summary>
        /// Begins rendering to texture
        /// </summary>
        public void BeginRenderToTexture()
        {
            mRenderTexture.PrepareViewport();
        }

        /// <summary>
        /// Draws a texture to the rendering texture
        /// </summary>
        public void DrawTexture(ETexture texture, int x, int y, int width, int height)
        {
            mRenderTexture.DrawTexture(x, y, width, height, texture as OpenTKTexture);
        }

        /// <summary>
        /// Draws a rect to the rendering texture
        /// </summary>
        public void DrawRect(int x, int y, int width, int height, EVector4 color)
        {
            mRenderTexture.DrawRect(x, y, width, height, color.ToOpenTK());            
        }

        /// <summary>
        /// Ends drawing to the rendering texture
        /// TO-DO: Lighting
        /// </summary>
        public ETexture EndRenderToTexture()
        {
            mRenderTexture.RestoreViewport(mGLControl.Width, mGLControl.Height, mLightingEnabled);

            // render the background
            if (mBackground != null)
            {
                mBackground.Render(mGLControl.Width, mGLControl.Height, 0, -0.9999f, mLightingEnabled);
            }               

            return mRenderTexture;
        }

        /// <summary>
        /// Not used here
        /// </summary>
        public void InvokeRendering()
        {
            throw new NotImplementedException();
        }


        #endregion

        #region Handlers

        /// <summary>
        /// Paint handler
        /// </summary>
        private void mGLControl_Paint(object sender, System.Windows.Forms.PaintEventArgs e)
        {
            // invoke the request of re-rendering of the scene
            if (RenderRequestEvent != null)
            {
                RenderRequestEvent(this, EventArgs.Empty);
            }
        }

        /// <summary>
        /// Mouse down event
        /// </summary>
        private void mGLControl_MouseDown(object sender, System.Windows.Forms.MouseEventArgs e)
        {
            if (e.Button == System.Windows.Forms.MouseButtons.Left)
            {
                if (LeftButtonDownEvent != null)
                {
                    EMouseEventArgs args = new EMouseEventArgs();
                    args.Position = new System.Windows.Point(e.Location.X, e.Location.Y);
                    LeftButtonDownEvent(this, args);
                }
            }
            else if (e.Button == System.Windows.Forms.MouseButtons.Right)
            {
                if (RightButtonDownEvent != null)
                {
                    EMouseEventArgs args = new EMouseEventArgs();
                    args.Position = new System.Windows.Point(e.Location.X, e.Location.Y);
                    RightButtonDownEvent(this, args);
                }
            }
        }

        /// <summary>
        /// Mouse move event
        /// </summary>
        private void mGLControl_MouseMove(object sender, System.Windows.Forms.MouseEventArgs e)
        {
            if (MouseMoveEvent != null)
            {
                EMouseEventArgs args = new EMouseEventArgs();
                args.Position = new System.Windows.Point(e.Location.X, e.Location.Y);
                MouseMoveEvent(this, args);
            }            
        }

        /// <summary>
        /// Mouse up event
        /// </summary>
        private void mGLControl_MouseUp(object sender, System.Windows.Forms.MouseEventArgs e)
        {
            if (e.Button == System.Windows.Forms.MouseButtons.Left)
            {
                if (LeftButtonUpEvent != null)
                {
                    EMouseEventArgs args = new EMouseEventArgs();
                    args.Position = new System.Windows.Point(e.Location.X, e.Location.Y);
                    LeftButtonUpEvent(this, args);
                }
            }
            else if (e.Button == System.Windows.Forms.MouseButtons.Right)
            {
                if (RightButtonUpEvent != null)
                {
                    EMouseEventArgs args = new EMouseEventArgs();
                    args.Position = new System.Windows.Point(e.Location.X, e.Location.Y);
                    RightButtonUpEvent(this, args);
                }
            }
        }

        /// <summary>
        /// Event when mouse wheel is rotated
        /// </summary>
        private void mGLControl_MouseWheel(object sender, System.Windows.Forms.MouseEventArgs e)
        {
            if (MouseWheelEvent != null)
            {
                EMouseEventArgs args = new EMouseEventArgs();
                args.Position = new System.Windows.Point(e.Location.X, e.Location.Y);
                args.WheelDelta = e.Delta;

                MouseWheelEvent(this, args);
            }
        }

        /// <summary>
        /// Handling resize event of the rendering control
        /// </summary>
        private void mGLControl_Resize(object sender, EventArgs e)
        {
            // set the perspective matrix
            GL.Viewport(0, 0, mGLControl.Width, mGLControl.Height);
            GL.MatrixMode(MatrixMode.Projection);
            GL.LoadIdentity();
            Matrix4 perspective = Matrix4.CreatePerspectiveFieldOfView(mCamera.GetFOV(), (float)mGLControl.Width / (float)mGLControl.Height, mCamera.GetNear(), mCamera.GetFar());
            GL.MultMatrix(ref perspective);
        }

        #endregion

        #region Private methods

        /// <summary>
        /// Setups the viewport
        /// </summary>
        private void SetupViewport()
        {
            int w = mGLControl.Width;
            int h = mGLControl.Height;

            GL.EnableClientState(ArrayCap.VertexArray);
            GL.EnableClientState(ArrayCap.NormalArray);
            GL.EnableClientState(ArrayCap.TextureCoordArray);
            GL.Enable(EnableCap.DepthTest);
            GL.PixelStore(PixelStoreParameter.UnpackAlignment, 1);
            GL.Enable(EnableCap.Texture2D);
            GL.ShadeModel(ShadingModel.Smooth);

            GL.Hint(HintTarget.PerspectiveCorrectionHint, HintMode.Nicest);
            GL.Enable(EnableCap.Lighting);

            //GL.LightModel(LightModelParameter.LightModelAmbient, 0f);
            //GL.LightModel(LightModelParameter.LightModelLocalViewer, 1f);
            GL.LightModel(LightModelParameter.LightModelColorControl, (float)LightModelColorControl.SeparateSpecularColor);


            GL.MatrixMode(MatrixMode.Projection);
            GL.LoadIdentity();

            // do centering
            const double frustrumConst = 0.9d;

            int size = Math.Min(w, h);
            double left = -frustrumConst * ((double)w / (double)size);
            double right = frustrumConst * ((double)w / (double)size);

            GL.Frustum(left, right, -frustrumConst, frustrumConst, 3, 20);
            GL.Viewport(0, 0, w, h); // Use all of the glControl painting area
        }

        #endregion
    }
}
